const varint = require('./binary-varint');
const float = require('./binary-float');
const utf8 = require('./binary-utf8');
const base64 = require('./binary-base64');

const getType = exports.getType = function getType(param) {
    return Object.prototype.toString.call(param).slice(8, -1).toLowerCase();
}

const isObject = exports.isObject = function isObject(param) {
    return param && typeof param === 'object';
}

const isArray = exports.isArray = function isArray(param) {
    return getType(param) === 'array';
}

const isMap = exports.isMap = function isMap(param) {
    return getType(param) === 'object';
}

const isString = exports.isString = function isString(param) {
    return getType(param) === 'string';
}

const isNumber = exports.isNumber = function isNumber(param) {
    return getType(param) === 'number';
}

const isInteger = exports.isInteger = function isInteger(param) {
    return isNumber(param) && Number.isInteger(param);
}

const isFloat = exports.isFloat = function isFloat(param) {
    return isNumber(param) && !Number.isInteger(param);
}

const isBoolean = exports.isBoolean = function isBoolean(param) {
    return getType(param) === 'boolean';
}

const isBase64 = exports.isBase64 = function isBase64(param) {
    return isString(param) && base64.test(param);
}

const isEmpty = exports.isEmpty = function isEmpty(obj) {
    if (isArray(obj)) {
        return !obj.length;
    } else if (isMap(obj)) {
        for (const key in obj) {
            return false;
        }
    }
    return true;
}

exports.uint32 = {
    bytes: 0,
    encode(val, buffer, offset) {
        varint.write(val, buffer, offset);
        this.bytes = varint.bytes;
    },
    decode(buffer, offset) {
        const value = varint.read(buffer, offset);
        this.bytes = varint.bytes;
        return value;
    },
    length(val) {
        return varint.length(val)
    }
}

exports.uint64 = exports.uint32;

exports.int32 = {
    bytes: 0,
    encode(val, buffer, offset) {
        varint.write(val < 0 ? val + 4294967296 : val, buffer, offset);
        this.bytes = varint.bytes;
    },
    decode(buffer, offset) {
        const value = varint.read(buffer, offset);
        this.bytes = varint.bytes;
        return value > 2147483647 ? value - 4294967296 : value;
    },
    length(val) {
        return varint.length(val < 0 ? val + 4294967296 : val)
    }
}

exports.int64 = {
    bytes: 0,
    encode(val, buffer, offset) {
        if (val < 0) {
            var last = offset + 9
            varint.write(val * -1, buffer, offset)
            offset += varint.bytes - 1
            buffer[offset] = buffer[offset] | 0x80
            while (offset < last - 1) {
                offset++
                buffer[offset] = 0xff
            }
            buffer[last] = 0x01
            this.bytes = 10
        } else {
            varint.write(val, buffer, offset)
            this.bytes = varint.bytes
        }
    },
    decode(buffer, offset) {
        var val = varint.read(buffer, offset)
        if (val >= Math.pow(2, 63)) {
            var limit = 9
            while (buffer[offset + limit - 1] === 0xff) limit--
            limit = limit || 9
            var subset = buffer.slice(offset, offset + limit)
            // var subset = Buffer.allocUnsafe(limit)
            // buffer.copy(subset, 0, offset, offset + limit)
            subset[limit - 1] = subset[limit - 1] & 0x7f
            val = -1 * varint.read(subset, 0)
            this.bytes = 10
        } else {
            this.bytes = varint.bytes
        }
        return val
    },
    length(val) {
        return val < 0 ? 10 : varint.length(val)
    }
}

exports.sint32 = {
    bytes: 0,
    encode(v, b, o) {
        v = v >= 0 ? v * 2 : v * -2 - 1
        varint.write(v, b, o)
        this.bytes = varint.bytes
    },
    decode(b, o) {
        const v = varint.read(b, o)
        this.bytes = varint.bytes
        return v & 1 ? (v + 1) / -2 : v / 2
    },
    length(v) {
        return varint.length(v >= 0 ? v * 2 : v * -2 - 1)
    }
};

exports.sint64 = exports.sint32;

exports.float = {
    bytes: 4,
    encode(val, buffer, offset) {
        float.writeFloatLE(val, buffer, offset)
    },
    decode(buffer, offset) {
        return float.readFloatLE(buffer, offset)
    },
    length(val) {
        return this.bytes;
    }
}

exports.double = {
    bytes: 8,
    encode(val, buffer, offset) {
        float.writeDoubleLE(val, buffer, offset)
    },
    decode(buffer, offset) {
        return float.readDoubleLE(buffer, offset)
    },
    length(val) {
        return this.bytes;
    }
}

exports.boolean = {
    bytes: 1,
    encode(val, buffer, offset) {
        varint.write(val ? 1 : 0, buffer, offset)
    },
    decode(buffer, offset) {
        return !!varint.read(buffer, offset)
    },
    length(val) {
        return this.bytes;
    }
}

const buffer_array = [];
const string = exports.string = {
    bytes: 0,
    encode(val, buffer, offset) {
        const oldOffset = offset;

        const len = utf8.write(val, buffer_array, 0);
        varint.write(len, buffer, offset)
        offset += varint.bytes;

        for (let index = 0; index < len; index++) {
            buffer[offset++] = buffer_array[index];
        }

        buffer_array.length = 0;

        this.bytes = offset - oldOffset;
    },
    decode(buffer, offset) {
        const oldOffset = offset;

        const len = varint.read(buffer, offset);
        offset += varint.bytes;

        this.bytes = offset + len - oldOffset;

        return utf8.read(buffer, offset, offset + len);
    },
    length(val) {
        const len = utf8.length(val);
        return len + varint.length(len);
    }
}

exports.base64 = {
    bytes: 0,
    encode(val, buffer, offset) {
        const oldOffset = offset;

        const len = base64.write(val, buffer_array, 0);
        varint.write(len, buffer, offset)
        offset += varint.bytes;

        for (let index = 0; index < len; index++) {
            buffer[offset++] = buffer_array[index];
        }

        buffer_array.length = 0;

        this.bytes = offset - oldOffset;
    },
    decode(buffer, offset) {
        const oldOffset = offset;

        const len = varint.read(buffer, offset);
        offset += varint.bytes;

        this.bytes = offset + len - oldOffset;

        return base64.read(buffer, offset, offset + len);
    },
    length(val) {
        const len = base64.length(val);
        return len + varint.length(len);
    }
}

exports.object = {
    bytes: 0,
    encode(val, buffer, offset) {
        if (isEmpty(val)) {
            string.encode('', buffer, offset);
        } else {
            string.encode(JSON.stringify(val), buffer, offset);
        }
        this.bytes = string.bytes;
    },
    decode(buffer, offset) {
        const str = string.decode(buffer, offset);
        this.bytes = string.bytes;
        return str ? JSON.parse(str) : {};
    },
    length(val) {
        const len = isEmpty(val) ? 0 : utf8.length(JSON.stringify(val));
        return len + varint.length(len);
    }
}

exports.array = {
    bytes: 0,
    encode(val, buffer, offset) {
        if (isEmpty(val)) {
            string.encode('', buffer, offset);
        } else {
            string.encode(JSON.stringify(val), buffer, offset);
        }
        this.bytes = string.bytes;
    },
    decode(buffer, offset) {
        const str = string.decode(buffer, offset);
        this.bytes = string.bytes;
        return str ? JSON.parse(str) : [];
    },
    length(val) {
        const len = isEmpty(val) ? 0 : utf8.length(JSON.stringify(val));
        return len + varint.length(len);
    }
}